对象属性的自动序列化与反序列化
(给CPP开发者加星标,提升C/C++技能)
来源:CSDN - ZJU_fish1996 https://blog.csdn.net/ZJU_fish1996/article/details/102643222
对于一个有着多个属性的类对象而言,我们通常希望能够对其进行序列化与反序列化,以保存和导入我们记录下来的物体数据。编写这样的代码通常是繁琐的,并且会带来大量冗余。
目标
我们期望能够达到这样的效果,在类中声明变量的时候,能够自动注册相关的信息。在序列化和反序列化的过程中,该变量的值就会自动被解析,而无需额外的编码。
这意味着,我们的代码可以按如下形式编写,达到自动序列化/反序列化的目的:
class Object
{
protected:
QOpenGLShaderProgram* program = nullptr;
public:
REGISTER_SERIALIZE
Property_Param (Vector3f, m_f3Position, Vector3f(0,0,0))
Property_Param (Vector3f, m_f3Rotation, Vector3f(0,0,0))
Property_Param (Vector3f, m_f3Scale, Vector3f(1,1,1))
Property_Param (bool, m_bCastShadow, true)
Property_Param (bool, m_bRender, true)
Property_Param (float, m_fAlpha, 1.0f)
Property_Param_Func (int, m_nRenderPriority, -1, OnInitRenderPriority)
Property_Func (string, m_strObjName, OnLoadObj)
Property (string, m_strName)
Property (Shape, m_shape )
Property (string, m_strType)
Property (int, m_nId )
void OnInitRenderPriority(const int& value);
void OnLoadObj(const string& name);
virtual void UpdateLocation();
virtual void Create() = 0;
virtual void Render() { }
virtual void Draw(bool bTess = false);
virtual void Draw(QOpenGLShaderProgram*,bool bTess = false);
virtual ~Object() { }
};
特别地,派生类的写法也基本类似:
class PBRObject : public Object
{
private:
void SetImage(const string& name, QImage& image, GLuint& texId);
public:
~PBRObject () override;
GLuint m_nAlbedo = 0;
GLuint m_nNormal = 0;
GLuint m_nMaskTex = 0;
QImage m_imgAlbedo;
QImage m_imgNormal;
QImage m_imgMask;
Property_Func (string, m_strAlbedo, SetImage, m_imgAlbedo, m_nAlbedo)
Property_Func (string, m_strNormal, SetImage, m_imgNormal, m_nNormal)
Property_Func (string, m_strMask, SetImage, m_imgMask, m_nMaskTex)
Property_Param (Vector3f, m_f3Color, Vector3f(1,1,1))
Property_Param (float, m_fAo, 0.4f)
Property_Param (float, m_fRough, 0.0f)
Property_Param (float, m_fMetal, 0.0f)
Property_Param (bool, m_bFire, false)
Property_Param (bool, m_bBloom, false)
Property_Param (bool, m_bSSR, false)
Property_Param (bool, m_bXRay, false)
Property_Param (bool, m_bOutline, false)
Model* pModel = nullptr;
void Create() override;
void Render() override;
};
实现细节
我采取的方案是宏定义 + 模板编程。
对象的序列化有着多种格式,最为常见的是键值对的存储方式,类似Xml,Json或者flatbuffers(当然我们可以选择将其存为文本格式或二进制格式),相比起直接把所有值按序存储的暴力方式,它的好处在于添加和移除一些对象并不会影响数据的读取,非常适合应用于一个可能需要不断更新的应用。
本文中,选择了Xml作为存储格式。
序列化注册
首先,为了能够把每个变量对象加入到序列化管理,我们有必要定义一个管理类,那么它应该有一个容器,存储所有的变量名字以及对应的数据;具备加入数据的方法;支持数据的序列化与反序列化:
class CSerializeHelper
{
public:
class BaseObject
{
// ...
}; // 数据格式定义
void PushBack(BaseObject* obj) // 支持添加数据
{
// ...
}
void Serialize(QDomElement& child) // 序列化
{
// ...
}
void Deserialize(QDomElement& child) // 反序列化
{
// ...
}
private:
list<BaseObject*> listObjs; // 数据容器
};
BaseObject中记录了每个变量的一些辅助数据。
之后,为了快速在类中注册这一序列化管理类,我们定义如下宏,在类中直接引用即可:
#define REGISTER_SERIALIZE \
CSerializeHelper serializeHelper; \
void Save(QDomElement& child) \
{ \
serializeHelper.Serialize(child); \
} \
void Load(QDomElement& child) \
{ \
serializeHelper.Deserialize(child); \
} \
接下来,我们定义变量,以下是最为简单的定义变量的宏:
#define Property(type, name) \
type name; \
当我们在类中编写形如:
Property(int,x)
时,我们实际上就得到了如下的代码:
int x;
序列化对象管理
但这也仅仅定义了变量,我们还没有将其加入到序列化管理中。为了管理该对象,首先我们需要考虑到,对象可以有很多类型,所以我们需要使用泛型编程,也就是将CSerializeHelper中容器管理的对象定义为泛型对象。
其中,m_strName记录了变量名字的字符串形式,unique_ptr<T> m_value记录了对象当前的值的引用。
此外,提供getStrValue() 和 setStrValue()的接口来实现泛型数据到字符串之间的转化,便于序列化。
class BaseObject
{
public:
BaseObject(const string& inName)
: m_strName(inName) { }
virtual string getStrName() final { return m_strName; }
virtual string getStrValue() = 0;
virtual void setStrValue(const string& strValue) = 0;
virtual ~BaseObject() = 0;
private:
string m_strName;
};
template<typename T>
class Param : public BaseObject
{
public:
Param() { }
// ...
private:
unique_ptr<T> m_value;
}
此时,CSerializeHelper中的三个方法可以定义如下:
class CSerializeHelper
{
public:
// ...
void PushBack(BaseObject* obj)
{
listObjs.push_back(obj);
}
void Serialize(QDomElement& child)
{
for(BaseObject* obj : listObjs)
{
child.setAttribute(QString::fromStdString(obj->getStrName()),
QString::fromStdString(obj->getStrValue()));
}
}
void Deserialize(QDomElement& child)
{
for(BaseObject* obj : listObjs)
{
QString attributeName = QString::fromStdString(obj->getStrName());
if(child.hasAttribute(attributeName))
{
QString strValue = child.attribute(attributeName);
obj->setStrValue(strValue.toStdString());
}
}
}
};
为了能够在定义变量的同时将变量加入序列化管理,我们在变量声明的同时,生成一个变量数据辅助类BaseObject的实例,此时Propery()宏扩展如下(备注:#代表字符串化,##代表连接):
#define Property(type, name) \
type name; \
CSerializeHelper::Param<type>* name##Param \
= new CSerializeHelper::Param<type> \
(#name, &name, serializeHelper); \
同时,在构造函数中完成相应的操作(把数据加入序列化管理):
template<typename T>
class Param : public BaseObject
{
public:
Param(const string& inName, T* inValue, CSerializeHelper& helper)
: BaseObject(inName), m_value(inValue)
{
helper.PushBack(this);
}
// ...
};
此时,对于Property(int, x)而言,我们实际得到了如下代码:
int x;
CSerializeHelper::Param<int>* xParam
= new CSerializeHelper::Param<int>
("x", &x, serializeHelper);
序列化实现(字符串和任意类型相互转换)
至此,我们已经在类中注册了序列化管理类,生成保存和导入的函数,并能够将不同类型的变量自动加入管理维护。剩余的一个工作就是实现任意类型到字符串的相互转换。大部分情况下,我们都可以利用stringstream来辅助这一实现:
template<typename T>
class Param : public BaseObject
{
public:
string getStrValue() override
{
if(m_value)
{
stringstream ss;
ss << *(m_value);
return ss.str();
}
return string();
}
void setStrValue(const string& strValue) override
{
T res;
stringstream ss(str);
ss >> res;
}
};
对于内置类型,基本上可以直接使用;对于自定义类型,只需重载operator<<和>>,具备比较好的扩展性。例如,对于自定义类型Vector3f,需要添加如下两个运算符重载:
struct Vector3f
{
float x,y,z;
Vector3f() { }
Vector3f(float _x,float _y,float _z) :x(_x),y(_y),z(_z) { }
friend ostream& operator<<(ostream& out, const Vector3f& vec)
{
out << vec.x << " " <<vec.y << " " << vec.z ;
return out;
}
friend istream& operator>>(istream& in, Vector3f& vec)
{
in >> vec.x >> vec.y >> vec.z;
return in;
}
};
但也有一些例外。比如对于我们自定义的枚举类型,我们就无法为其重载运算符。为了照顾这些特殊情况,我们第一个考虑到的可能是模板特例化,而枚举类型是一类类型,并不是单个类型,这意味我们需要对每一个自定义枚举类型做特例化,这样意味着每次我们添加新的自定义枚举类型时,还需要修改框架代码。
实际上,我们需要的是有条件的编译,也就是在条件A时生成函数A,而在条件B时生成函数B。根据类型的不同,来得到不同的字符串转换方法。我们可以利用enable_if特性来实现:
template<typename T>
typename enable_if<is_enum<T>::value, T>::type
strConvert(const string& str)
{
int res = strConvert<int>(str);
return static_cast<T>(res);
}
template<typename T>
typename enable_if<!is_enum<T>::value, T>::type
strConvert(const string& str)
{
T res;
stringstream ss(str);
ss >> res;
return res;
}
上述代码的含义是:若T类型为enum,生成第一个函数,我们将enum视为int来处理;否则,生成第二个函数。
对于其余可能存在的特殊情况,我们也可以采用这种方法实现,此时,我们改进了setStrValue的实现:
template<typename T>
class Param
{
public:
// ...
void setStrValue(const string& strValue) override
{
*m_value = strConvert<T>(strValue);
}
};
绑定回调函数
在有些情况下,在类中声明变量,我们希望为其赋值初值;又有些情况下,我们希望自定义赋值的过程,或者说,在赋值的同时完成一些别的操作。比如当我们读入一个图像的名字时,我们希望能够同时加载这张图像,存到另一个变量中。
我们扩展了变量声明的宏,同时也支持设定初始值的写法:
#define Property_Param(type, name, arg) \
type name = arg; \
CSerializeHelper::Param<type>* name##Param \
= new CSerializeHelper::Param<type> \
(#name, &name, serializeHelper); \
为了完成回调,我们需要传入一个函数,记录在BaseObject中,在加载数据的时候回调。为了实现这一目的,我们扩展宏如下:
#define Property_Func(type, name, func) \
type name; \
CSerializeHelper::Param<type>* name##Param \
= new CSerializeHelper::Param<type> \
(#name, &name, serializeHelper, \
[&](type val){func(val);}); \
同时在构造函数中支持函数的传入:
template<typename T>
class Param : public BaseObject
{
// ...
public:
Param(const string& inName, T* inValue, CSerializeHelper& helper)
: BaseObject(inName), m_value(inValue)
{
helper.PushBack(this);
}
Param(const string& inName, T* inValue, CSerializeHelper& helper, function<void(T)> onInit)
: Param(inName, inValue, helper)
{
m_funcOnInit = onInit;
}
void setStrValue(const string& strValue) override
{
if(m_funcOnInit)
{
m_funcOnInit(strConvert<T>(strValue));
}
else
{
*m_value = strConvert<T>(strValue);
}
}
private:
// ...
function<void(T)> m_funcOnInit;
};
这样,我们就能支持形如void(T)类型的函数回调了。但这可能是不够的,有时候我们还希望传入其它参数,同样地以图片地址为例,我们希望传入一个图像对象,把结果绑定到特定的对象中。我们假定第一个参数始终是T,如果希望支持传入任意类型的函数参数,我们需要使用可变长模板,传入一个模板参数包:
#define Property_Func(type, name, func, ...) \
type name; \
CSerializeHelper::Param<type>* name##Param \
= new CSerializeHelper::Param<type> \
(#name, &name, serializeHelper, \
[&](type val){func(val,__VA_ARGS__);}); \
template<typename T,typename... Args>
class Param : public BaseObject
{
public:
// ...
Param(const string& inName, T* inValue, CSerializeHelper& helper, function<void(T, Args...)> onInit)
: Param(inName, inValue, helper)
{
m_funcOnInit = onInit;
}
private:
// ...
function<void(T, Args...)> m_funcOnInit;
};
此时,我们就可以按照如下的写法,传入一个SetImage的回调函数,同时传入QImage和QLuint类型的参数,在回调函数中完成导入图片,并生成纹理id的操作:
class PBRObject : public Object
{
// ...
GLuint m_nAlbedo = 0;
QImage m_imgAlbedo;
Property_Func (string, m_strAlbedo, SetImage, m_imgAlbedo, m_nAlbedo)
void SetImage(const string& name, QImage& image, GLuint& texId);
};
入口
完成了上述一系列参数后,我们需要设置一个入口来调用以上序列化/反序列化函数。
对于保存而言,我们遍历所有Object对象,为其生成子结点,并交由每个对象自己填充child结点。
void ObjectInfo::Save(const QString& fileName)
{
QFile file(fileName);
if(!file.open(QFile::WriteOnly|QFile::Truncate))
return;
QDomDocument doc;
QDomProcessingInstruction instruction;
instruction = doc.createProcessingInstruction("xml", "version=\"1.0\" encoding=\"UTF-8\"");
doc.appendChild(instruction);
QDomElement root = doc.createElement("objectlist");
doc.appendChild(root);
for(size_t i = 0;i < vecObjs.size(); i++)
{
QDomElement child = doc.createElement("object");
vecObjs[i]->Save(child);
root.appendChild(child);
}
QTextStream out_stream(&file);
doc.save(out_stream,4);
file.close();
}
对于导入而言,我们需要先生成一个对象,然后才能让每个对象读入数据。为了能够生成对象,我们首先从xml中读入类型。此时的类型为字符串格式,这里需要我们实现从字符串构造对象,这是另外一个课题,实现在这篇文章中提及:根据字符串自动构造对应类。
void ObjectInfo::Load(const QString& fileName)
{
QFile file(fileName);
if(!file.open(QFile::ReadOnly))
{
qDebug() << "fail open";
return;
}
QString errorStr;
int errorLine;
int errorColumn;
QDomDocument doc;
if (!doc.setContent(&file, false, &errorStr, &errorLine, &errorColumn))
{
qDebug() << "Error: Parse error at line " << errorLine << ", "
<< "column " << errorColumn;
return;
}
QDomElement root = doc.documentElement();
if (root.tagName() != "objectlist")
{
qDebug() << "failed load object list";
return;
}
QDomNode child = root.firstChild();
while (!child.isNull())
{
QDomElement element = child.toElement();
if (element.tagName() == "object")
{
QString type = element.attribute("m_strType");
if(!type.isEmpty())
{
shared_ptr<Object> obj = CreateObject(type.toStdString());
if(obj)
{
obj->Load(element);
}
}
}
child = child.nextSibling();
}
}
代码
.h
#ifndef OBJECTPROPERTY_H
#define OBJECTPROPERTY_H
#include <QtXml/QDomDocument>
#include <QtXml/QDomElement>
#include <sstream>
#include <list>
#include <memory>
#include <functional>
using namespace std;
template<typename T>
typename enable_if<is_enum<T>::value, T>::type
strConvert(const string& str)
{
int res = strConvert<int>(str);
return static_cast<T>(res);
}
template<typename T>
typename enable_if<!is_enum<T>::value, T>::type
strConvert(const string& str)
{
T res;
stringstream ss(str);
ss >> res;
return res;
}
class CSerializeHelper
{
public:
class BaseObject
{
public:
BaseObject(const string& inName)
: m_strName(inName) { }
virtual string getStrName() final { return m_strName; }
virtual string getStrValue() = 0;
virtual void setStrValue(const string& strValue) = 0;
virtual ~BaseObject() = 0;
private:
string m_strName;
};
template<typename T,typename... Args>
class Param : public BaseObject
{
public:
Param(const string& inName, T* inValue, CSerializeHelper& helper)
: BaseObject(inName), m_value(inValue)
{
helper.PushBack(this);
}
Param(const string& inName, T* inValue, CSerializeHelper& helper, function<void(T, Args...)> onInit)
: Param(inName, inValue, helper)
{
m_funcOnInit = onInit;
}
string getStrValue() override
{
if(m_value)
{
stringstream ss;
ss << *(m_value);
return ss.str();
}
return string();
}
void setStrValue(const string& strValue) override
{
if(m_funcOnInit)
{
m_funcOnInit(strConvert<T>(strValue));
}
else
{
*m_value = strConvert<T>(strValue);
}
}
~Param() override { }
private:
unique_ptr<T> m_value;
function<void(T, Args...)> m_funcOnInit;
};
void PushBack(BaseObject* obj)
{
listObjs.push_back(obj);
}
void Serialize(QDomElement& child)
{
for(BaseObject* obj : listObjs)
{
child.setAttribute(QString::fromStdString(obj->getStrName()),
QString::fromStdString(obj->getStrValue()));
}
}
void Deserialize(QDomElement& child)
{
for(BaseObject* obj : listObjs)
{
QString attributeName = QString::fromStdString(obj->getStrName());
if(child.hasAttribute(attributeName))
{
QString strValue = child.attribute(attributeName);
obj->setStrValue(strValue.toStdString());
}
}
}
private:
list<BaseObject*> listObjs;
};
#define Property(type, name) \
type name; \
CSerializeHelper::Param<type>* name##Param \
= new CSerializeHelper::Param<type> \
(#name, &name, serializeHelper); \
#define Property_Param(type, name, arg) \
type name = arg; \
CSerializeHelper::Param<type>* name##Param \
= new CSerializeHelper::Param<type> \
(#name, &name, serializeHelper); \
#define Property_Func(type, name, func, ...) \
type name; \
CSerializeHelper::Param<type>* name##Param \
= new CSerializeHelper::Param<type> \
(#name, &name, serializeHelper, \
[&](type val){func(val,__VA_ARGS__);}); \
#define Property_Param_Func(type, name, arg, func, ...) \
type name = arg; \
CSerializeHelper::Param<type>* name##Param \
= new CSerializeHelper::Param<type> \
(#name, &name, serializeHelper, \
[&](type val){func(val,__VA_ARGS__);}); \
#define REGISTER_SERIALIZE \
CSerializeHelper serializeHelper; \
void Save(QDomElement& child) \
{ \
serializeHelper.Serialize(child); \
} \
void Load(QDomElement& child) \
{ \
serializeHelper.Deserialize(child); \
} \
#endif // OBJECTPROPERTY_H
cpp:
string CSerializeHelper::BaseObject::getStrValue() { return string(); }
CSerializeHelper::BaseObject::~BaseObject() { }
- EOF -
1、一次蓝屏故障分析
2、共享智能指针探究
关注『CPP开发者』
看精选C++技术文章 . 加C++开发者专属圈子
↓↓↓
点赞和在看就是最大的支持❤️